﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Diagnostics.Contracts;
using System.Text.RegularExpressions;

namespace SetVersionNumber {

	/// <summary>
	/// This class actually contains the high-level file-version update logic.
	/// </summary>
	public class FileVersionUpdater {

		/// <summary>
		/// Create a new file version updater
		/// </summary>
		public FileVersionUpdater( ) {
			this.ImportComponents();
		}


		/// <summary>
		/// Uses MEF to import all the components required by this instance.
		/// </summary>
		private void ImportComponents( ) {
			// We'll search in all the assemblies where this program is located
			var assemblyDir = Assembly.GetExecutingAssembly().Location;
			var catalog = new AggregateCatalog();
			catalog.Catalogs.Add(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
			catalog.Catalogs.Add(new DirectoryCatalog(Path.GetDirectoryName(assemblyDir)));

			// Create the CompositionContainer with our catalogue of DLLs
			var container = new CompositionContainer(catalog);

			// Ask the container to satisfy our imports
			container.ComposeParts(this);
		}


		// ReSharper disable UnusedMember.Local
		[ContractInvariantMethod]
		private void ObjectInvariants( ) {
			Contract.Invariant(this.FileUpdaters!=null, "The file updaters should not be null");
			Contract.Invariant(this.VersionStrategies!=null, "The file updaters should not be null");
		}
		// ReSharper restore UnusedMember.Local


		/// <summary>
		/// The collection of available file updaters
		/// </summary>
		[ImportMany(AllowRecomposition=true)]
		public IEnumerable<IFileUpdater> FileUpdaters { get; set; }


		/// <summary>
		/// The collection of available version strategies that
		/// can be used to update the version number in a file
		/// </summary>
		[ImportMany(AllowRecomposition=true)]
		public IEnumerable<IVersionStrategy> VersionStrategies { get; set; }


		/// <summary>
		/// Find the strategy with the given name.
		/// </summary>
		/// <param name="name">The name of the strategy to search for</param>
		/// <returns>
		/// The strategy with the given name, or <c>null</c> if 
		/// </returns>
		public IVersionStrategy FindStrategyByName( string name ) {
			if( string.IsNullOrWhiteSpace(name) ) return null;
			foreach( var strategy in this.VersionStrategies )
				if( string.Equals(strategy.Name, name, StringComparison.CurrentCultureIgnoreCase) )
					return strategy;
			return null;
		}


		/// <summary>
		/// Finds all updatable files in the current directory.
		/// </summary>
		/// <param name="directory">
		/// The directory to search in; if this is <c>null</c> then the current directory is used.
		/// </param>
		/// <returns>
		/// The collection of files that could be updated by one of the file updaters.
		/// </returns>
		public IEnumerable<string> FindAllUpdatableFiles( string directory=null ) {
			Contract.Ensures(Contract.Result<IEnumerable<string>>() != null);

			// The "default value" for directory is the current directory
			if( string.IsNullOrWhiteSpace(directory) )
				directory = Environment.CurrentDirectory;

			// Find all the files that one of the file updaters could process
			var results = new List<string>();
			foreach( var updater in this.FileUpdaters ) {
				var files = from f in Directory.EnumerateFiles(directory)
							where updater.WillProcessFile(f)
							select f;
				results.AddRange(files);
			}//foreach updater
			return results.Distinct();
		}


		/// <summary>
		/// Update the given files using the given version number strategy.
		/// </summary>
		/// <param name="strategy">The version strategy to use to create the new version number</param>
		/// <param name="filesToUpdate">
		/// The collection of files to update; if this is <c>null</c> then all the updatable files
		/// in the current working directory will be updated.
		/// </param>
		public int UpdateVersionNumbersInFile( IVersionStrategy strategy, IEnumerable<string> filesToUpdate=null ) {
			// Check pre-conditions
			Contract.Requires(strategy!=null, "The update strategy must not be null.");
			Contract.Ensures(Contract.Result<int>()>=0, "Zero or more files must be processed");
			if( filesToUpdate==null || filesToUpdate.Count()==0 )
				filesToUpdate = this.FindAllUpdatableFiles();

			// Now update all of the files
			int filesUpdated = 0;
			foreach( var fileName in filesToUpdate ) {
				var updater = this.FindUpdater(fileName);
				if( updater==null ) {
					Console.WriteLine("No updater available for: " + fileName);
					continue;
				}//if - no updater

				// We have an updater, try updating the file
				bool wasUpdated = updater.ProcessFile(fileName, strategy);
				if( wasUpdated ) {
					filesUpdated += 1;
					Console.WriteLine("Updated " + fileName);
				}
				else
					Console.WriteLine("Failed to update: " + fileName);
			}//foreach - file to update

			return filesUpdated;
		}


		/// <summary>
		/// Finds the first updater that can process the given file.
		/// </summary>
		/// <param name="fileName">The file name of the file to be processed.</param>
		/// <returns>
		/// A file updater that can process the given file, or <c>null</c> if no suitable file 
		/// updater could be found.
		/// </returns>
		private IFileUpdater FindUpdater( string fileName ) {
			return (from updater in this.FileUpdaters
					where updater.WillProcessFile(fileName)
					select updater).FirstOrDefault();
		}

	}//class

}//namespace
